#!/bin/bash

# This script builds upon ufw (Ubuntu Firewall) and hooks rules into
# the ufw-user-input chain.  These rules make up a port knocking
# system. On my VPS, the sshd daemon listens on a fixed TCP port, but
# I keep the port closed most of the time.  However, after packets on
# the right successive 'GATE' ports, the firewall opens up the sshd
# port for 30 seconds to the source IP of those packets.
#
# A GATE is a portnumber in range [1,65535]. PROT is either 'tcp' or
# 'udp'.  Each hour of the day is mapped to 3 port/protocol
# combinations by the algorithm of this script.
#
# The algorithm uses a seed to be supplied to the script by means of
# environment variable KNOCKSEED. KNOCKSEED must hold a numeric
# value.  The port for sshd is read from environment variable
# SSHDPORT. SSHDPORT defaults to 22 if not supplied.
#
# This script is supposed to be called from crontab hourly, so the
# right GATE/PROT combination is configured for the hour to come.
# Example crontab entry (for root):
#
# @hourly SSHDPORT=22222 KNOCKSEED=42 /root/bin/knocksystem.sh downup
#
# This script is accompanied with an example client script that
# generates the knocks needed to trigger the firewall: knock.sh It
# needs KNOCKSEED.  The original idea for this script was found here:
# https://www.digitalocean.com/community/tutorials/how-to-configure-port-knocking-using-only-iptables-on-an-ubuntu-vps
# The page explains how the 'recent' module is used. I expanded on the
# idea and made some small improvements.
#


###########################################################################
# Enable knock system
###########################################################################
if [ "X$1" == "Xup" ] ; then


   # validate KNOCKSEED
   re='^[+-]?[0-9]+([.][0-9]+)?$'
   if ! [[ $KNOCKSEED =~ $re ]] ; then
      echo KNOCKSEED must hold a numeric value. Exit 1. >&2 ; exit 1
   fi


   # validate SSHDPORT
   if [ -z "$SSHDPORT" ]  ; then
      echo SSHDPORT not supplied. Using SSHDPORT=22 >&2
      SSHDPORT=22
   else
      re='^[0-9]+$'
      if ! [[ $SSHDPORT =~ $re ]] || [[ $SSHDPORT -gt 65535 ]] || [[ $SSHDPORT -lt 1 ]] ; then
         echo SSHDPORT must be a portnumber. Exit 1. >&2 ; exit 1
      fi
   fi


   # Generate three GATE/PROT combinations
   declare -a GATE
   declare -a PROT
   dd=`date +"%Y%m%d%H00"`
   d5=`echo $((dd+KNOCKSEED)) | md5sum`

   for i in 1 2 3 ; do

      s=$((1+5*(i-1)))

      gate=`echo $d5 | cut -c$s- | cut -c-4`
      GATE[$i]=`printf "%d" $((0x$gate % 32767 + 32768 ))`

      prot=`echo $d5 | cut -c$((s+4))`
      prot=`printf "%d" $((0x$prot % 2))`
      if [ $prot -eq 0 ] ; then PROT[$i]='tcp' ; else PROT[$i]='udp' ; fi

   done


   # Create netfilter rules
   IPT=/usr/sbin/iptables
   $IPT -N PASSS ; $IPT -N GATE3 ; $IPT -N GATE2 ; $IPT -N GATE1 ; $IPT -N KNOCK
   $IPT -A ufw-user-input -j KNOCK

   $IPT -A KNOCK -m recent --rcheck --seconds 30 --name AUTH3 -j PASSS
   $IPT -A KNOCK -m recent --rcheck --seconds 30 --name AUTH2 -j GATE3
   $IPT -A KNOCK -m recent --rcheck --seconds 30 --name AUTH1 -j GATE2
   $IPT -A KNOCK -j GATE1

   $IPT -A GATE1 -j ufw-logging-deny
   $IPT -A GATE1 -p ${PROT[1]} -m ${PROT[1]} --dport ${GATE[1]} -m recent --set --name AUTH1 -j DROP
   $IPT -A GATE1 -j DROP

   $IPT -A GATE2 -m recent --name AUTH1 --remove
   $IPT -A GATE2 -p ${PROT[2]} -m ${PROT[2]} --dport ${GATE[2]} -j ufw-logging-deny
   $IPT -A GATE2 -p ${PROT[2]} -m ${PROT[2]} --dport ${GATE[2]} -m recent --set --name AUTH2 -j DROP
   $IPT -A GATE2 -j GATE1

   $IPT -A GATE3 -m recent --name AUTH2 --remove
   $IPT -A GATE3 -p ${PROT[3]} -m ${PROT[3]} --dport ${GATE[3]} -j ufw-logging-deny
   $IPT -A GATE3 -p ${PROT[3]} -m ${PROT[3]} --dport ${GATE[3]} -m recent --set --name AUTH3 -j DROP
   $IPT -A GATE3 -j GATE2

   $IPT -A PASSS -m recent --name AUTH3 --remove
   $IPT -A PASSS -p tcp --dport ${SSHDPORT} -j ACCEPT
   $IPT -A PASSS -j GATE3


###########################################################################
# Disable knock system
###########################################################################
elif [ "X$1" == "Xdown" ] ; then
   IPT=/usr/sbin/iptables
   $IPT -F PASSS ; $IPT -F GATE3 ; $IPT -F GATE2 ; $IPT -F GATE1 ; $IPT -F KNOCK 
   $IPT -D ufw-user-input -j KNOCK
   $IPT -X PASSS ; $IPT -X GATE3 ; $IPT -X GATE2 ; $IPT -X GATE1 ; $IPT -X KNOCK


###########################################################################
# Disable and re-enable knock system
###########################################################################
elif [ "X$1" == "Xdownup" ] ; then
   $0 down
   $0 up


###########################################################################
# Check status by showing relevant rule contents
###########################################################################
elif [ "X$1" == "Xstatus" ] ; then
   /usr/sbin/iptables-save     | \
	   egrep  ufw-user-input\|KNOCK\|GATE.\|PASSS


###########################################################################
# Show packet counts (for debugging)
###########################################################################
elif [ "X$1" == "Xpackets" ] ; then
   /usr/sbin/iptables-save -c  | \
	   grep -v '\[0:0\]'   | \
	   sed 's/:.*\]/\t/'   | \
	   sed 's/^\[//'       | \
	   egrep  ufw-user-input\|KNOCK\|GATE.\|PASSS


###########################################################################
# Usage message
###########################################################################
else
   echo "Usage: $0 [up|down|downup|status|packets]"


fi

